Topology Spread Constraints
개요
파드 토폴로지 분산은 파드를 여러 노드로 분산 배치를 하고 싶을 때 사용하는 스케줄링 기법이다.
워크로드를 배포하는데 전부 같은 노드에 배치되는 것은 고가용성에 있어 안전하지 않다.
그래서 이들을 가능한 다양한 노드에 배치하고 싶을 때 이 기능을 사용하면 좋다.
사전 지식 - 토폴로지
여기에서 토폴로지를 구태여 자세히 설명하고 있으므로, 나도 여기에 살짝 맞추고자 한다.
토폴로지라고 하는 것은 같은 라벨을 가진 노드의 그룹이다.
클러스터 내에서 노드들을 스펙이나 실제 물리적 위치 등을 고려하여 그룹화를 시켜 활용할 수 있다.
가령 서울 리전에 있는 레디스와 상호작용을 해야 하는 웹서버는 미국 리전보다는 서울 리전에 설치되는 것이 트래픽 비용 절감에 속도 향상도 추구할 수 있을 것이다.
이때, 각 리전의 노드들에 topology.kubernetes.io/region: {리전}
이라고 라벨을 붙여준다면?
그럼 이제 topology.kubernetes.io/region
라벨 키를 기준으로 서로 다른 값들을 가진 노드들을 그룹화시킬 수 있다.
이런 식으로 라벨을 기준으로 노드를 나누는 것이 바로 토폴로지이다.
표현은 저마다 다양하지만, 보통 토폴로지의 한 단위 그룹은 도메인이라고도 부른다.
(나는 보통 그냥 한 토폴로지 그룹이라고 표현하는 편이다)
토폴로지는 운영환경에서 효율성, 안전성을 고려할 때 중요하게 다뤄지는 요소로서 쿠버네티스에서도 이에 기반하여 다양한 기능을 제공하고 있다.
대표적인 것 중, 스케줄링에는 토폴로지 분산 제약 설정이 있고, 네트워크 라우팅에는 토폴로지 기반 라우팅이 있다.
양식 작성법
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
topologySpreadConstraints:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: test
maxSkew: 1
whenUnsatisfiable: [DoNotSchedule | ScheduleAnyway]
minDomains: 1 # 옵션
matchLabelKeys: <list> # 옵션
nodeAffinityPolicy: [Honor|Ignore] # 옵션
nodeTaintsPolicy: [Honor|Ignore] # 옵션
파드에 topologySpreadConstraints
필드를 이용해 설정할 수 있다.
하위로 리스트를 작성하는 것을 보면 알 수 있듯이, 여러 개의 제약 조건을 걸 수 있다.
각 필드가 무얼 뜻하는지 보자.
- topologyKey
- 노드의 그룹을 나누는 기준이 되는 노드 라벨 키를 말한다.
- 해당 라벨 키에 대해 같은 값을 가지는 노드들은 하나의 그룹으로 묶이며, 이를 도메인이라고 부른다.
- labelSelector
- 분산할 때 고려할 파드들을 매칭하기 위한 라벨 셀렉터
- maxSkew
- 파드가 도메인 간 편향(skew)되게 분포될 수 있는 최대치를 나타낸다.
- 즉, 편향도(skew)는 도메인 별로 분류했을 때 파드 최대 개수 - 파드 최소 개수로 계산된다.
- 이 값이 1이라면, a 노드에 파드가 1개 배치됐다면 다른 노드는 최대 2개까지만 파드를 배치할 수 있다.
- whenUnsatisfiable
- 위의 조건이 충족될 수 없다면 스케줄링을 어떡할지 행동을 지정한다.
- DoNotSchedule의 경우엔 필터링이 걸려 아예 스케줄이 진행되지 않는다.
- ScheduleAnyway의 경우 이 설정은 스코어링으로 여겨지며 높은 값이 매겨진다.
- minDomains
- 여기에서 도메인은 토폴로지로 묶이는 하나의 그룹을 칭한다.
- 그래서 파드가 분산될 때 최소 얼마나 많은 그룹에 분산돼야 하는지를 지정한다.
- whenUnsatisfiable가 DoNotSchedule일 때만 사용할 수 있다.
- matchLabelKeys
- 라벨 셀렉터만으로는 부족할 때 사용하는 필드.
- affinity matchLabelsKey 참고.
- nodeAffinityPolicy
- 노드 어피니티, 노드 셀렉터 정보를 편향도 계산 시 어떻게 할 지 정책을 지정한다.
- Honor의 경우 어피니티로 매칭된 노드만 계산에 활용한다.
- Ignore의 경우 그냥 모든 노드로 계산한다.
- nodeTaintsPolicy
- 노드 테인트, 톨러레이션 정보를 편향 계산 시 어떻게 할 지 정책을 지정한다.
- Honor의 경우 해당 파드가 테인트, 톨러레이션으로 실제 배치될 수 있는 노드들에 대해서만 계산에 활용한다.
- Ignore의 경우 그냥 모든 노드로 계산한다.
minDomains
최소 도메인 개수, 즉 최소한 얼마나 많은 토폴로지 도메인에 파드가 배치돼야 하는지를 지정하는 필드가 왜 있는 것일까?
물론 옵션으로 사용하는 필드이긴 한데, 이게 정말 필요할까?
다음의 상황을 생각해보자.
(토폴로지만 고려하고자 노드를 표현하지 않았다.)
현재 zone은 2개만 있는 상태이고 maxSkew를 1로 잡았다.
이때 새로운 파드가 배치된다면, 두 zone 중에 하나에 배치가 될 것이다.
그러기보다는 조금 더 고가용성을 추구하기 위해 새로운 zone이 생기고 그곳에 파드가 배치되었으면 한다.
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
minDomains: 3
labelSelector:
matchLabels:
foo: bar
그럼 이런 식으로 설정을 해주면 파드는 스케줄할 수 없는(unschedulable) 상태가 되고, 이를 통해 새로운 zone을 만들도록 압박할 수 있다.
(이 압박을 받는 대상은 Karpenter, Cluster Autoscaler와 같은 클러스터 오토스케일링 툴이다.)
minDomains이 존재할 때는 어떤 일이 일어날까?
현재 도메인 개수는 a,b 2개이다.
그러나 minDomains는 3이므로, 도메인 개수가 더 낮다.
이 경우에는 maxSkew가 계산될 때, 평소와 다르게 계산 로직이 동작한다.
위 예시에서는 원래 파드 최소 개수는 2개여야 하나, minDomains가 실제 도메인 개수보다 크기에 파드 최소 개수가 0으로 무조건 고정된다.
결과적으로 편향도 = 2 - 0 = 2로 maxSkew 값을 넘기게 되어 파드는 Pending 상태가 돼버린다.
이를 통해 클러스터는 새로운 도메인이 만들어져야만 파드를 배치할 수 있게 되는 것이다.
minDomains는 이런 식의 설정을 할 수 있도록 하기 위해 디자인됐다.[1]
보통이라면 그냥 배치되고 끝났을 문제를 막음으로써 자연스럽게 클러스터가 오토스케일링될 수 있도록 만들어주는 것이다.
당연히 minDomains을 이미 충족하는 상황이라면 이런 식으로 동작하지 않는다.
이 정도 설명만으로는 조금 부족하다고 느껴질 수 있으니, 사용 케이스를 고려해보자.
유즈 케이스
여러 분산 제약 설정
현재 foo: bar
이란 라벨이 붙은 파드가 이런 식으로 퍼져 있고 새로운 파드를 하나 띄워야 하는 상황이라 생각해보자.
zone을 토폴로지로 삼을 때, 우리는 새로운 파드가 기왕이면 zoneB에 배치되길 희망한다.
metadata:
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
그렇다면 이런 식으로 토폴로지 분산 설정을 할 수 있다.
zone을 기반으로 토폴로지를 잡았으니 일단 zoneA와 zoneB가 각 도메인으로 분리된다.
이 상태에서 maxSkew가 1이니, 이제 우리도 파드가 어디에 배치될지 알 수 있다.
- zoneA에 파드가 배치된다면?
- zoneA 파드 3개 - zoneB 파드 1개 = 2
- 즉, 최대편향도값을 넘어버리므로, zoneA에는 배치될 수 없다.
- zoneB에 파드가 배치된다면?
- zoneA 파드 2개 - zoneB 파드 2개 = 0
- 최대편향도값을 넘지 않으므로, zoneB에는 배치될 수 있다!
그래서 새로운 파드는 zoneB에 배치가 될 것이다.
다만 zoneB에 해당하는 노드 중 어떤 노드에 배치될지는 쿠버 조상님도 모른다..
그래서 기왕이면 아무런 파드가 배치되지 않은 노드4에 배치되는 것이 이상적일 것이다!
metadata:
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
- masSkew: 1
topologyKey: node
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
이럴 때는 이렇게 추가적인 분산 조건을 걸어주면, 각 노드 별로도 토폴로지 그룹이 형성되어 확실하게 노드4로 파드가 배치될 것을 보장할 수 있다!
이렇게 활용할 수 있기 때문에 여러 개의 분산 제약을 걸 수 있도록 디자인된 것이다.
제약 조건 충돌
다만, 이런 케이스도 있을 수 있다.
zone으로 분산을 걸기는 하지만 사실 zone마다 노드의 개수가 다를 수 있다.
위의 정책을 그대로 적용하게 된다면, zoneB가 파드가 2개이므로 이쪽에 파드가 배치된다.
그러나 노드 간 최대 편향도는 1을 넘어가면 안되므로 실상 제약 조건 간의 충돌이 일어나 파드가 스케줄링되지 못한다.
이러한 상황을 방지하려면 zone마다 노드 개수가 다를 수 있음을 가정하고 애초에 zone에 대한 maxSkew
값을 조금 더 크게 주는 것이 좋을 것이다.
파드 안티 어피니티와의 비교
분산을 하는 설정을 하는 것은 좋다!
그런데, 이런 설정은 사실 pod AntiAffinity로도 달성할 수 있지 않을까?
실제로도 그렇다.[2]
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: gitlab-runner
이 설정은 사실..
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: "app"
operator: "In"
values:
- "gitlab-runner"
topologyKey: "kubernetes.io/hostname"
이렇게 파드 안티 어피니티로도 똑같은 효과를 달성할 수 있다.
여기서부터는 조금 내 생각이다.
파드 안티 어피니티와 비교하여 토폴로지 분산 제약의 장점은 maxSkew에 있다.
안티 어피니티를 세팅할 때 require를 이용해 완벽하게 파드들이 떨어지게도 만들 수 있고, 위 예시처럼 prefer를 이용해 가능한 떨어지게 만드는 것도 가능하다.
그러나, prefer를 사용할 때는 weight 값을 얼마나 줘야 할지를 잘 조절해야 하며, 또한 이것은 결국 정말 파드가 분산되는 것을 명확하게 보장할 수는 없다.
반면 토폴로지 분산 제약은 최대 편향도를 지정하여 다른 노드에 비해 한 노드가 지나치게 파드를 배치받는 것을 명확하게 막을 수 있다.
그래서 큰 규모의 클러스터를 운영할 때 관리를 간편하게 하기 위해서는 토폴로지 분산 제약을 사용하는 것이 아무래도 유리하다.
반면 파드 안티 어피니티는 단순하게 한 노드에 두 워크로드를 배치하지 않고 싶을 때 간단하게 사용하기에 좋다.
주의사항
파드 분산 제약은 기본적으로 같은 네임스페이스의 파드들만을 대상으로 잡는다.
(나중에 어피니티처럼 네임스페이스를 고를 수 있게 설정이 추가될지도 모르겠다.)
의도된 게 아니라면 배치되는 파드 역시 topologySpreadConstraints.labelSelector
에 해당하는 라벨을 가지도록 하자.
이렇게 하지 않으면 파드가 배치되더라도 결국 편향도에는 아무런 변화가 생기지 않고, 같은 짓을 반복하더라도 실제 스케줄링에 아무런 변화가 생기지 않게 된다.
또한 이 설정은 스케줄링에 대해서만 영향을 끼치기에 파드가 삭제될 때에 대해서는 아무런 영향을 주지 않는다.
클러스터 전역 설정
kube-scheduler 프로필 설정에서 아예 토폴로지 분산 제약 설정을 넣는 것도 가능하다.
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
defaultingType: List
설정을 할 때는 파드 라벨에 대한 값만 빼고 모든 것을 미리 세팅할 수 있다.
토폴로지 분산 제약 세팅을 하지 않은 파드들에 대해서는 이 값이 적용되게 된다.
args:
defaultConstraints:
- maxSkew: 3
topologyKey: "kubernetes.io/hostname"
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 5
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnyway
아무런 설정을 하지 않을 시, 기본으로 적용되는 설정은 이렇다.
보다시피 모든 노드에 파드간 차이가 3개 이상 벌어지지 않도록 스케줄링하도록 돼있다.
관련 문서
이름 | noteType | created |
---|---|---|
Topology Spread Constraints | knowledge | 2025-03-19 |